/**
 * 
 */
package org.dragonfire.dasm.instr;

import java.util.ArrayList;

import org.dragonfire.dasm.Command;
import org.dragonfire.dasm.CompileException;
import org.dragonfire.dasm.DASM;
import org.dragonfire.dasm.Listing;
import org.dragonfire.dasm.postprocessor.Postprocessor;
import org.dragonfire.util.DMath;
import org.dragonfire.util.DString;

import com.stevesoft.pat.RegRes;
import com.stevesoft.pat.Regex;
import com.stevesoft.pat.ReplaceRule;
import com.stevesoft.pat.StringBufferLike;

/**
 * An instruction is the basis of any assembler language. Each
 * <code>Instruction</code> has a mnemonic, a fixed size, parameters, and the
 * binary machine code into which the instruction is eventually compiled down.
 * DASM allows instruction mnemonics to be overloaded, using differences in the
 * parameter types to choose which <code>Instruction</code> to use. Thus,
 * creating two instructions with the same or matching parameter types will
 * cause only one to be used and should be avoided because which
 * <code>Instruction</code> is used may not only be indeterminent but may
 * change in future versions. Untyped parameters default to literals (type "<code>L</code>")
 * and more information about parameter typing can be found at the
 * {@link org.dragonfire.dasm.instr.CommandDef} class. For more information on
 * how to create commands in source files, see the <code>#</code>{@link org.dragonfire.dasm.directives.Instruction}
 * class.<br />
 * <br />
 * Binary Strings:<br />
 * <br />
 * Each command has a binary string of <code>1</code>'s, <code>0</code>'s
 * and parameter inputs that tells the compiler how to compile each instruction.
 * Each parameter input has the following format:<br />
 * <code>&lt;<i>&lt;index&gt;</i>:<i>&lt;size&gt;</i>[,<i>&lt;rule&gt;</i>]&gt;</code><br />
 * Parameters:<br />
 * <table>
 * <tr>
 * <td><code>index</code></td>
 * <td>(Integer) the index of the parameter to use for the input.</td>
 * </tr>
 * <tr>
 * <td><code>size</code></td>
 * <td>(Integer) The size of the variable in bytes, or of the register in bits,
 * depending upon the type of parameter at the input's index.</td>
 * </tr>
 * <tr>
 * <td><code>rule</code></td>
 * <td>(Integer, Optional) The name of the <code>Rule</code> to use before
 * inserting the binary into the final code.</td>
 * </tr>
 * </table>
 * 
 * @author Darth Android
 * @date Nov 30, 2006
 * @version 2.0
 */
public class Instruction extends CommandDef {

	/**
	 * The default Rule to use when inserting a param into a binary string when
	 * one is not specified.
	 */
	public static final String DEFAULT_RULE = "NOP";
	/**
	 * The binary machine code for this <code>Instruction</code>.
	 */
	private String bin = "";
	/**
	 * The final size of this <code>Instruction</code>, in bytes.
	 */
	private int size = 0;

	/**
	 * Creates a new <code>Instruction</code> with the given values.
	 * 
	 * @param name
	 *            The mnemonic or name of the <code>Instruction</code>.
	 * @param params
	 *            The parameters this <code>Instruction</code> requires.
	 * @param binary
	 *            The binary code for this <code>Instruction</code>.
	 * @param size
	 *            The final size of the compiled instruction, which must be
	 *            greater than 0.
	 * @throws IllegalArgumentException
	 *             If <code>size</code> is less than 1.
	 */
	public Instruction(String name, String[] params, String binary, int size) {
		super(name, validateInstructionParams(params));
		if (size < 1)
			throw new IllegalArgumentException("Size may not be less than 1");
		bin = binary;
		this.size = size;
	}

	public ArrayList<Listing> pass1(Command c) {
		if (c.getParams().length != getParamFormats().length)
			throw new IllegalArgumentException("Parameter length doesn't match up.");
		ArrayList<Listing> list = new ArrayList<Listing>();
		Listing l = new Listing(size, c);
		list.add(l);
		l.setInstruction(this);
		return list;
	}

	public void pass2(Listing l, Command c) {
		if (l.getInstruction() != this)
			throw new IllegalArgumentException("This Listing was not built from this Instruction's pass1() method.");
		if (l.getSize() != size)
			throw new CompileException("ERROR_INSTR_SIZE", l.getSize(), size);
		final Postprocessor post = getCompiler().getPostprocessor();
		String paramsRaw = c.getRawParams();
		final String[] params = DString.splitTopLevel(paramsRaw, ',');
		class InstReplaceRule extends ReplaceRule {

			public void apply(StringBufferLike sb, RegRes r) {
				int index = getCompiler().evalInt(r.stringMatched(1)) - 1;
				String paramDef = getParamFormats()[index];
				String[] defParts = DString.trim(DString.splitTopLevel(paramDef, ':'));
				String param = DString.trim(params[index]);
				int size = post.getCompiler().evalInt(defParts[1]);
				char paramType = defParts[0].charAt(0);
				String newBits = "";
				switch (paramType) {
					case 'P':
						param = param.substring(1, param.length() - 1);
					case 'R':
						int regClass = post.getCompiler().evalInt(defParts[2]);
						Register reg = post.getCompiler().getInstructions().getRegister(param, regClass);
						if (reg.getBits().length() != size)
							throw new CompileException("ERROR_INVALID_REGISTER", param);
						newBits = reg.getBits();
						break;
					case 'O':
						param = param.substring(1, param.length() - 1);
						String[] parts = param.split("\\+", 2);
						String register = parts[0];
						regClass = post.getCompiler().evalInt(defParts[2]);
						reg = post.getCompiler().getInstructions().getRegister(register, regClass);
						int val = post.getCompiler().evalInt(parts[1]);
						String bits = Integer.toBinaryString(val);
						while (bits.length() < size)
							bits = (val < 0 ? "1" : "0") + bits;
						boolean truncated = false;
						while (bits.length() > size) {
							if (bits.charAt(0) != (val < 0 ? '1' : '0'))
								truncated = true;
							bits = bits.substring(1);
						}
						if (truncated)
							post.getCompiler().warn("WARN_TRUNCATED_DATA", val, DMath.ordinal(index + 1));
						newBits = reg.getBits() + ":" + bits;
						break;
					case 'M':
						param = param.substring(1, param.length() - 1);
					case 'V':
						int value = post.getCompiler().evalInt(param);
						newBits = Integer.toBinaryString(value);
						while (newBits.length() < size)
							newBits = (value < 0 ? "1" : "0") + newBits;
						truncated = false;
						while (newBits.length() > size) {
							if (newBits.charAt(0) != (value < 0 ? '1' : '0'))
								truncated = true;
							newBits = newBits.substring(1);
						}
						if (truncated)
							post.getCompiler().warn("WARN_TRUNCATED_DATA", value, DMath.ordinal(index + 1));
						break;
					default: // should never get here
						throw new RuntimeException("ILLEGAL PARAMETER TYPE");
				}
				String SRule = r.stringMatched(2);
				if (SRule == null || SRule.equals(""))
					SRule = DEFAULT_RULE;
				Rule rule = Rule.getInstance(SRule);
				newBits = rule.evalParam(post, newBits);
				sb.append(newBits);
			}
		}
		Regex insR = new Regex("\\<(\\d+)(?::((??word)))?\\>", new InstReplaceRule());
		String bin = insR.replaceAll(getBinaryString());
		if (!bin.matches("[01]*"))
			throw new CompileException("ERROR_INSTR_NOT_BINARY", bin);
		if (bin.length() / 8 != size)
			throw new CompileException("ERROR_INSTR_SIZE", size, bin.length() / 8.0);
		for (int i = 0; i < size; i++) {
			String bits = bin.substring(i * 8, (i + 1) * 8);
			byte b = (byte) Integer.parseInt(bits, 2);
			l.getMCode()[i] = b;
		}
	}

	public String getBinaryString() {
		return bin;
	}

	/**
	 * Returns the final size of this instruction
	 * 
	 * @return The final size of this instruction
	 */
	public int getSize() {
		return size;
	}

	private static String[] validateInstructionParams(String[] paramTypes) {
		if (paramTypes == null)
			paramTypes = new String[0];
		for (int i = 0; i < paramTypes.length; i++) {
			paramTypes[i] = validateInstructionParam(paramTypes[i]);
		}
		return paramTypes;
	}

	private static String validateInstructionParam(String paramType) {
		if (paramType == null)
			throw new IllegalArgumentException("paramType may not be null!");
		if (!paramType.contains(":")) {
			paramType = "L:" + paramType;
		}
		String[] parts = DString.splitTopLevel(paramType, ':');
		parts = DString.trim(parts);
		if (parts.length < 1 || parts[0].length() != 1)
			throw new CompileException("ERROR_PARAM_TYPE_MALFORMED", paramType);
		char type = parts[0].toUpperCase().charAt(0);
		if (parts.length < 2)
			throw new CompileException("ERROR_PARAM_TYPE_TOO_SHORT", type + "", 2);
		try {
			switch (type) {
				case 'O':
					if (parts.length < 3)
						throw new CompileException("ERROR_PARAM_TYPE_TOO_SHORT", type + "", 3);
					int Offsize = new Integer(parts[1]);
					int OffclassID = new Integer(parts[2]);
					paramType = "O:" + Offsize + ":" + OffclassID;
					break;
				case 'M':
					int addrSize = new Integer(parts[1]);
					paramType = "M:" + addrSize;
					break;
				case 'V':
					int varSize = new Integer(parts[1]);
					paramType = "V:" + varSize;
					break;
				case 'P':
					if (parts.length < 3)
						throw new CompileException("ERROR_PARAM_TYPE_TOO_SHORT", type + "", 3);
					int ptrSize = new Integer(parts[1]);
					int ptrClass = new Integer(parts[2]);
					paramType = "P:" + ptrSize + ":" + ptrClass;
					break;
				case 'R':
					if (parts.length < 3)
						throw new CompileException("ERROR_PARAM_TYPE_TOO_SHORT", type + "", 3);
					int size = new Integer(parts[1]);
					int classID = new Integer(parts[2]);
					paramType = "R:" + size + ":" + classID;
					break;
				case 'L':
					String text = parts[1];
					if (!text.matches(DASM.LITERAL_REGEX))
						throw new CompileException("ERROR_INVALID_LITERAL", text);
					paramType = "L:" + text;
					break;
				default:
					throw new CompileException("ERROR_INVALID_PARAM_TYPE", type + "");
			}
		} catch (NumberFormatException ne) {
			throw new CompileException("ERROR_INVALID_PARAM_TYPE_SIZE", type + "", parts[1]);
		}
		return paramType;
	}
}
